#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import sys
import argparse
import subprocess

import ctools.common.mycmd as mycmd
from ctools.common.strhelper import *
from ctools.common.ifilter import *

from ctools.datahelper.cflowhelper import CFlowHelper
from ctools.datahelper.functree import (SNode,)
from ctools.converters.conv_sn import ConvSN2Gv, ConvSN2C
from ctools.datahelper.texthelper import TreeTextParser
import ctools.gvhelper.render as gvrender
from ctools.gvhelper.graphwrapper import GraphWrapper

from ctools.subcmd.ctbase import *

import logging
from ctools.logext import *

_rlog, _plog, _mlog, _tlog = get_loggers(__file__, __name__, propagate=False)

_fpath = os.path.dirname(os.path.realpath(__file__))
_rcs_path = os.path.join(_fpath, '../rcs')

_prompt = 'CFlow=> '

class CFlowCli(mycmd.MyCmd):
    def __init__(self, prompt=_prompt,
                tryshell=False, autorep=True,
                comment_prefix='#',
                **kwargs):
        super().__init__(prompt=prompt,
                        tryshell=tryshell, autorep=autorep,
                        comment_prefix=comment_prefix)
        params = {
            'files':    [],
            'depth':    1,
            'show_mode': 'tree',
        }
        self._params.update(params)
        if kwargs:
            self._params.update(kwargs)

        self._cflow = CFlowHelper()
        self._ifilter = iFilter()
        self._tparser = TreeTextParser()
        self._sn2gv = ConvSN2Gv()
        self._sn2c = ConvSN2C()
        self._render = gvrender.GvRender()

        self._sn2gv.set_filter(self._ifilter)

        ignore_txt = os.path.join(_rcs_path, 'ignore.txt')
        if os.path.exists(ignore_txt):
            self._ifilter.loadtxt(ignore_txt, is_ignore=True)

        # setup argparser of commands
        for name in dir(self.__class__):
            if name.startswith('_setupargp_'):
                func = getattr(self, name)
                func()

    def _check_files(self) -> bool:
        return len(self.param_value('files', ())) > 0

    def _setupargp_addfiles(self):
        argp = SubcmdArgp(add_help=False)

        group = argp.add_mutually_exclusive_group(required=True)
        group.add_argument("-f", "--files", action='store_true', help="files")
        group.add_argument("-l", "--filelist", action='store_true', help="filelist")
        group.add_argument("-c", "--cmd", action='store_true', help="shell command")

        argp.add_argument("params", type=str)

        self.help_addfiles = lambda : print(argp.format_help())
        self._argp_addfiles = argp

    def _add_file(self, filename):
        if not os.path.isfile(filename):
            return False
        try:
            if filename in self.param_value('files'):
                return False
            self._params['files'].append(filename)
        except KeyError:
            self._params['files'] = [filename]
        return True

    def do_addfiles(self, line:str):
        try:
            argv = line.strip().split(maxsplit=1)
            args = self._argp_addfiles.parse_args(argv)
        except ArgumentError as e:
            print(e)
            return

        cnt = 0
        if args.files:
            for f in args.params.split():
                cnt += 1 if self._add_file(f) else 0

        if args.filelist:
            for f in args.params.split():
                if not os.path.isfile(f):
                    continue
                with open(f, 'r') as fp:
                    for line in fp:
                        line = line.strip()
                        if not line:
                            continue
                        if line.startswith('#'):
                            continue
                        cnt += 1 if self._add_file(f) else 0

        if args.cmd:
            ret = subprocess.run(args.params, stdout=subprocess.PIPE,
                                 shell=True, text=True)
            if ret.returncode != 0:
                print(ret.stderr)
                return

            for f in ret.stdout.splitlines():
                cnt += 1 if self._add_file(f) else 0

        print(f'add {cnt} files, now {len(self.param_value("files"))} files')

    def _snode(self, symbols, depth) -> SNode:
        buf = self._cflow.do_calltree(
            self._params['files'],
            symbols, depth
        )
        _plog.debug(buf)
        rootnode = self._tparser.do_parse_text(buf)
        if rootnode.name:
            if rootnode.name not in symbols:
                return SNode()
        else:
            for node in rootnode.children[:]:
                if node.name not in symbols:
                    rootnode.rm_item(node)
                    # TODO: return SNode()?
        return rootnode

    def _setupargp_show(self):
        argp = SubcmdArgp(add_help=False)

        argp.add_argument('-m', '--mode', type=str,
                          choices=('tree', 'c', 'gv'),
                          default=self.param_value('show_mode', 'tree'))
        argp.add_argument('-d', '--depth', type=int,
                          default=self.param_value('depth', 1))
        argp.add_argument('symbols', nargs='*')
        self._argp_show = argp

    def help_show(self):
        # use 'show -h' would cause break break, use 'help show'
        print(self._argp_show.format_help())

    def do_show(self, line):
        if not self._check_files():
            print("no files")
            return

        try:
            args = self._argp_show.parse_args(line.split())
        except ArgumentError as e:
            print(e)
            return

        snode = self._snode(args.symbols, args.depth)

        if args.mode == 'tree':
            snode.dump()
        elif args.mode == 'c':
            print(indent_str(1, self._sn2c.convert(snode)))
        elif args.mode == 'gv':
            print(indent_str(1, self._sn2gv.convert(snode)))

    def _setupargp_draw(self):
        argp = SubcmdArgp(add_help=False)

        argp.add_argument("-e", "--engine", type=str,
                        choices=("text",
                                "dot", "neato", "twopi", "circo", "fdp", "sfdp"),
                        default="dot")
        argp.add_argument("--no-render", dest='no_render', action='store_true')
        argp.add_argument("-o", "--output", type=str, required=True)
        argp.add_argument("-f", "--format", type=str, default=None)

        argp.add_argument('-d', '--depth', type=int,
                          default=self.param_value('depth', 1))
        argp.add_argument('symbols', nargs='*')

        self.help_draw = lambda : print(argp.format_help())
        self._argp_draw = argp

    def _do_draw(self, snode:SNode, opts:str=''):
        text = """digraph G {
    graph [rankdir=LR];
    graph [bgcolor="LightCyan:LightSteelBlue", gradientangle=300];
    splines = "polyline";
    node [shape=box, width=2];
    edge [minlen=2, penwidth=1];
"""
        text += opts
        text += '\n'
        text += indent_str(1, self._sn2gv.convert(snode))
        text += "}\n"
        return text

    def do_draw(self, line):
        if not self._check_files():
            print("no files")
            return

        try:
            args = self._argp_draw.parse_args(line.split())
        except ArgumentError as e:
            print(e)
            return

        snode = self._snode(args.symbols, args.depth)
        text = self._do_draw(snode)

        if args.engine == 'text' and args.no_render:
            with open(args.output, 'w') as fout:
                fout.write(text)
                print(f"save to {args.output}")
            return

        graph = GraphWrapper(string=text)
        if not args.no_render:
            self._render.render_all(graph)

        if args.engine == 'text':
            graph.write(args.output)
        else:
            graph.draw(args.output,
                       format=args.format, prog=args.engine)

class CFlowCmd(CToolsCmd):
    @classmethod
    def name(cls):
        return "cflow"

    @classmethod
    def description(cls):
        return "cflow cli tool"

    def do_command(self, argv:list[str]):
        cli = CFlowCli(prompt=_prompt)
        return cli.cmdloop()

if __name__ == '__main__':
    CFlowCmd().do_command(sys.argv[1:])
